import tkinter as tk
from tkinter import ttk, messagebox, PhotoImage, filedialog, simpledialog
import socket
import os
from datetime import datetime
import csv
import time
import re
from PIL import Image, ImageTk

class DL950SocketSCPIApp:
    def __init__(self, root):
        self.root = root
        self.root.title("DL950 Sample Code")

        self.sock = None
        self.ip_address = tk.StringVar(value="192.168.10.10")
        self.port = 10002

        self.channel_subchannel_checks = {}
        self.area1_param_checks = {}

        self.log_data = []

        # Area 1 Time Range variables
        self.area1_start = tk.StringVar(value="-5.00")
        self.area1_end = tk.StringVar(value="5.00")
        self.area1_mode_on = tk.BooleanVar(value=False) # Represents global MEASure:MODE state

        # Visibility variables for collapsible sections
        self.area1_options_visible = tk.BooleanVar(value=True)
        # REMOVED: self.area2_options_visible = tk.BooleanVar(value=True) 
        self.load_setup_options_visible = tk.BooleanVar(value=True)

        # Entry widgets (initialized to None, then assigned in setup_ui)
        self.area1_start_entry = None
        self.area1_end_entry = None
        
        self.setup_filename_entry = None

        # Logo handling variables
        self.logo_label = None
        self.logo = None # Store ImageTk.PhotoImage object here to prevent garbage collection

        self.setup_ui()

    def setup_ui(self):
        # Configure root grid for 2 rows and 2 columns
        self.root.grid_rowconfigure(0, weight=0)
        self.root.grid_rowconfigure(1, weight=1)
        self.root.grid_columnconfigure(0, weight=7)
        self.root.grid_columnconfigure(1, weight=3)

        # Style configurations (kept for consistency with the original structure)
        style = ttk.Style()
        style.configure('Orange.TButton', background='orange', foreground='white',
                        font=('Segoe UI', 9, 'bold'), borderwidth=0)
        style.map('Orange.TButton',
                    background=[('active', 'darkorange'), ('pressed', 'orangered')],
                    foreground=[('active', 'white'), ('pressed', 'white')])

        style.configure('DarkBlue.TButton', background='darkblue', foreground='white',
                        font=('Segoe UI', 9, 'bold'), borderwidth=0)
        style.map('DarkBlue.TButton',
                    background=[('active', 'blue'), ('pressed', 'mediumblue')],
                    foreground=[('active', 'white'), ('pressed', 'white')])

        # --- Top Row: Logo and Title spanning the entire width ---
        self.top_bar_frame = ttk.Frame(self.root)
        self.top_bar_frame.grid(row=0, column=0, columnspan=2, sticky="ew", pady=5)
        # Configure columns within top_bar_frame for centering
        self.top_bar_frame.grid_columnconfigure(0, weight=1)
        self.top_bar_frame.grid_columnconfigure(1, weight=0)
        self.top_bar_frame.grid_columnconfigure(2, weight=1)
        # Configure rows within top_bar_frame so logo is above title
        self.top_bar_frame.grid_rowconfigure(0, weight=0)
        self.top_bar_frame.grid_rowconfigure(1, weight=0)

        # Initial logo loading attempt - Using the path provided by the user
        image_path = r"C:\Users\35001010\OneDrive - Yokogawa Electric Corporation\Pictures\yokogawa.png"
        self.logo_label = ttk.Label(self.top_bar_frame)
        self.logo_label.grid(row=0, column=1, sticky="", padx=5)

        self._load_image_into_label(image_path)

        title_label = ttk.Label(self.top_bar_frame, text="DL950 Sample Code", font=("Segoe UI", 16, "bold"))
        title_label.grid(row=1, column=1, sticky="", padx=5, pady=(0, 5))

        # --- Second Row, Left Section (70%): Functional Area ---
        self.functional_area_frame = ttk.Frame(self.root, relief="groove", borderwidth=1)
        self.functional_area_frame.grid(row=1, column=0, sticky="nsew", padx=5, pady=5)
        self.functional_area_frame.grid_rowconfigure(0, weight=1)
        self.functional_area_frame.grid_columnconfigure(0, weight=1)

        self.canvas = tk.Canvas(self.functional_area_frame, borderwidth=0, highlightthickness=0)
        self.scrollbar = ttk.Scrollbar(self.functional_area_frame, orient="vertical", command=self.canvas.yview)
        self.canvas.configure(yscrollcommand=self.scrollbar.set)

        self.canvas.grid(row=0, column=0, sticky="nsew")
        self.scrollbar.grid(row=0, column=1, sticky="nse")

        self.container = ttk.Frame(self.canvas)
        self.container_id = self.canvas.create_window((0, 0), window=self.container, anchor="nw")

        self.container.bind("<Configure>", self.on_frame_configure)
        self.canvas.bind("<Configure>", self.on_canvas_configure)

        # --- Second Row, Right Section (30%): Output Log ---
        self.log_panel = ttk.Frame(self.root, relief="groove", borderwidth=1)
        self.log_panel.grid(row=1, column=1, sticky="nsew", padx=5, pady=5)
        self.log_panel.grid_rowconfigure(0, weight=1)
        self.log_panel.grid_columnconfigure(0, weight=1)

        out_frame = ttk.LabelFrame(self.log_panel, text="📄 Output Log")
        out_frame.grid(row=0, column=0, sticky="nsew", padx=5, pady=5)
        out_frame.grid_rowconfigure(0, weight=1)
        out_frame.grid_columnconfigure(0, weight=1)

        self.output = tk.Text(out_frame, height=20, font=("Consolas", 10), state="disabled", wrap="word")
        self.output.grid(row=0, column=0, sticky="nsew")

        self.output.tag_configure("cmd", foreground="blue")
        self.output.tag_configure("resp", foreground="green")
        self.output.tag_configure("error", foreground="red")
        self.output.tag_configure("info", foreground="gray")

        log_scrollbar = ttk.Scrollbar(out_frame, command=self.output.yview)
        log_scrollbar.grid(row=0, column=1, sticky="ns")
        self.output.config(yscrollcommand=log_scrollbar.set)

        log_btn_frame = ttk.Frame(self.log_panel)
        log_btn_frame.grid(row=1, column=0, sticky="ew", padx=5, pady=5)
        tk.Button(log_btn_frame, text="🧹 Clear Log", command=self.clear_log).pack(side="right")
        tk.Button(log_btn_frame, text="💾 Save Log to CSV", command=self.save_log_to_csv).pack(side="right", padx=10)


        # --- Main controls (rest of the UI) packed into self.container within the functional_area_frame ---
        conn_frame = ttk.LabelFrame(self.container, text="🔌 DL950 Connection")
        conn_frame.pack(fill="x", padx=10, pady=5)
        ttk.Label(conn_frame, text="IP Address:").pack(side="left", padx=(10, 5))
        ttk.Entry(conn_frame, textvariable=self.ip_address, width=20).pack(side="left")
        # Changed to tk.Button for solid color control
        tk.Button(conn_frame, text="Connect", command=self.connect, bg="lightblue", fg="black", activebackground="lightblue", activeforeground="black", relief="flat").pack(side="left", padx=10)
        self.status_label = ttk.Label(conn_frame, text="🔴 Not connected", foreground="red")
        self.status_label.pack(side="left", padx=20)

        # New Load Setup Section
        self.load_setup_options_container = ttk.LabelFrame(self.container, text="📂 Load Setup")
        self.load_setup_options_container.pack(fill="x", padx=10, pady=5)

        # Changed to tk.Button for solid color control
        self.load_setup_toggle_btn = tk.Button(self.load_setup_options_container, text="▼ Hide Load Setup Options",
                                             command=lambda: self.toggle_load_setup_options(initial=False), bg="gray", fg="white", activebackground="gray", activeforeground="white", relief="flat")
        self.load_setup_toggle_btn.pack(pady=5)

        self.load_setup_inner_frame = ttk.Frame(self.load_setup_options_container)
        self.load_setup_inner_frame.pack(fill="x", padx=10, pady=5)

        ttk.Label(self.load_setup_inner_frame, text="Filename:").pack(side="left", padx=(10, 5))
        self.setup_filename = tk.StringVar(value="my_setup_file") # Default filename
        self.setup_filename_entry = ttk.Entry(self.load_setup_inner_frame, textvariable=self.setup_filename, width=30)
        self.setup_filename_entry.pack(side="left", fill="x", expand=True, padx=5)
        # Changed to tk.Button for solid color control
        tk.Button(self.load_setup_inner_frame, text="Load", command=self.load_setup_from_entry, bg="gray", fg="white", activebackground="gray", activeforeground="white", relief="flat").pack(side="left")

        acq_frame = ttk.LabelFrame(self.container, text="🎬 Acquisition Controls")
        acq_frame.pack(fill="x", padx=10, pady=5)
        # Changed to tk.Button for solid color control
        tk.Button(acq_frame, text="Start Acquisition", bg="green", fg="white", activebackground="green", activeforeground="white", relief="flat", command=self.start_acquisition).pack(
            side="left", padx=10, pady=5)
        # Changed to tk.Button for solid color control
        tk.Button(acq_frame, text="Stop Acquisition", bg="red", fg="white", activebackground="red", activeforeground="white", relief="flat", command=self.stop_acquisition).pack(
            side="left", padx=10, pady=5)

        scpi_frame = ttk.LabelFrame(self.container, text="📤 Manual SCPI Command")
        scpi_frame.pack(fill="x", padx=10, pady=5)
        entry_inner = ttk.Frame(scpi_frame)
        entry_inner.pack(fill="x", padx=5, pady=5)
        ttk.Label(entry_inner, text="SCPI Command:").pack(side="left")
        self.cmd_entry = ttk.Entry(entry_inner, width=50)
        self.cmd_entry.pack(side="left", fill="x", expand=True, padx=5)
        # Changed to tk.Button for solid color control
        tk.Button(entry_inner, text="Send", command=self.send_command, bg="gray", fg="white", activebackground="gray", activeforeground="white", relief="flat").pack(side="left")

        # --- Area 1 Options Section (now "Measure Mode") ---
        self.area1_options_container = ttk.LabelFrame(self.container, text="📊 Measure Mode")
        self.area1_options_container.pack(fill="x", padx=10, pady=5)

        # Changed to tk.Button for solid color control
        self.area1_toggle_btn = tk.Button(self.area1_options_container, text="▼ Hide Measure Mode Options",
                                             command=lambda: self.toggle_area_options(1), bg="gray", fg="white", activebackground="gray", activeforeground="white", relief="flat")
        self.area1_toggle_btn.pack(pady=5)

        self.area1_inner_frame = ttk.Frame(self.area1_options_container)
        self.area1_inner_frame.pack(fill="x", padx=10, pady=5)

        # --- UPDATED CHANNEL/SUBCHANNEL SECTION (8 Channels, 2 Subchannels each) ---
        ch_frame = ttk.LabelFrame(self.area1_inner_frame, text="Channels + Subchannels (1-8, Sch 1-2)")
        ch_frame.pack(side="left", padx=10, pady=5, fill="y", expand=False)
        
        self.all_channels_subchannels = []
        NUM_CHANNELS = 8
        CHANNELS_PER_COLUMN = 2 # Displays 2 channels (4 checkboxes) per column

        # Iterate over the 8 channels (1 to 8)
        for ch in range(1, NUM_CHANNELS + 1):
            
            # Determine column (0, 1, 2, 3, 0, 1, 2, 3)
            col = (ch - 1) % (NUM_CHANNELS // CHANNELS_PER_COLUMN)
            
            # Determine major row offset (0 for Ch 1-4, 2 for Ch 5-8)
            row_major_offset = 0 if ch <= (NUM_CHANNELS // 2) else 2
            
            # Iterate over 2 subchannels (1, 2)
            for sch in range(1, 3):
                # Determine minor row offset (0 for sch 1, 1 for sch 2)
                row_minor_offset = sch - 1 
                row = row_major_offset + row_minor_offset
                
                label = f"{ch}_{sch}"
                var = tk.BooleanVar()
                chk = ttk.Checkbutton(ch_frame, text=label, variable=var)
                
                # Grid layout: (4 columns x 4 rows)
                chk.grid(row=row, column=col, sticky="w", padx=5, pady=2)
                
                self.channel_subchannel_checks[(ch, sch)] = var
                self.all_channels_subchannels.append(chk)
        # --- END OF UPDATED CHANNEL/SUBCHANNEL SECTION ---

        area1_settings_frame = ttk.Frame(self.area1_inner_frame)
        area1_settings_frame.pack(side="left", fill="both", expand=True, padx=10)

        tr_frame_area1 = ttk.LabelFrame(area1_settings_frame, text="⏱️ Time Range (Divisions)")
        tr_frame_area1.pack(fill="x", pady=5)
        ttk.Label(tr_frame_area1, text="Start:").grid(row=0, column=0, padx=5, pady=2, sticky="w")
        self.area1_start_entry = ttk.Entry(tr_frame_area1, textvariable=self.area1_start, width=8)
        self.area1_start_entry.grid(row=0, column=1, padx=5, pady=2)
        ttk.Label(tr_frame_area1, text="End:").grid(row=0, column=2, padx=5, pady=2, sticky="w")
        self.area1_end_entry = ttk.Entry(tr_frame_area1, textvariable=self.area1_end, width=8)
        self.area1_end_entry.grid(row=0, column=3, padx=5, pady=2)

        # Mode ON/OFF buttons for Area 1
        area1_action_frame = ttk.Frame(tr_frame_area1)
        area1_action_frame.grid(row=0, column=4, padx=5, pady=2)

        # Changed to tk.Button for solid color control
        tk.Button(area1_action_frame, text="Set TR", command=self.set_area1_time_range, bg="gray", fg="white", activebackground="gray", activeforeground="white", relief="flat").pack(side="left", padx=(0, 2))
        tk.Button(area1_action_frame, text="Mode ON", command=self.set_area1_mode_on, bg="green", fg="white", activebackground="green", activeforeground="white", relief="flat").pack(side="left", padx=(0, 2))
        tk.Button(area1_action_frame, text="Mode OFF", command=self.set_area1_mode_off, bg="red", fg="white", activebackground="red", activeforeground="white", relief="flat").pack(side="left", padx=(0, 2))


        self.all_parameters = [
            "AMPLitude", "AVERage", "AVGFreq", "AVGPeriod", "BWIDth1", "BWIDth2",
            "DUTYcycle", "FALL", "FREQuency", "HIGH", "LOW", "MAXimum",
            "MIDDle", "MINimum", "NOVershoot", "NWIDth", "PERiod", "PNUMber",
            "POVershoot", "PTOPeak", "PWIDth", "RISE", "RMS", "SDEViation",
            "TY1Integ", "TY2Integ"
        ]

        param_area1_frame = ttk.LabelFrame(area1_settings_frame, text="Parameters (Area 1)")
        param_area1_frame.pack(fill="both", expand=True, pady=5)
        self._create_parameter_checkboxes(param_area1_frame, self.area1_param_checks)

        # Changed to tk.Button for solid color control
        tk.Button(area1_settings_frame, text="Query", bg="green", fg="white", activebackground="green", activeforeground="white", relief="flat", command=self.query_area1_measurements).pack(
              pady=5, padx=5, anchor="e")

        # Initialize visibility of collapsible sections
        self.toggle_area_entries() # Ensures entries are normal state
        self.toggle_area_options(1, initial=True)
        # REMOVED: self.toggle_area_options(2, initial=True)
        self.toggle_load_setup_options(initial=True)


    def _create_parameter_checkboxes(self, parent_frame, param_dict):
        num_columns = 4
        num_rows = (len(self.all_parameters) + num_columns - 1) // num_columns

        for i, pname in enumerate(self.all_parameters):
            row = i % num_rows
            col = i // num_rows
            var = tk.BooleanVar()
            chk = ttk.Checkbutton(parent_frame, text=pname, variable=var)
            chk.grid(row=row, column=col, sticky="w", padx=5, pady=2)
            param_dict[pname] = var

    def on_frame_configure(self, event):
        # This makes the scrollable area adjust to the width of the canvas
        self.canvas.itemconfig(self.container_id, width=self.canvas.winfo_width())
        # This updates the scroll region based on the container's contents
        self.canvas.configure(scrollregion=self.canvas.bbox("all"))


    def on_canvas_configure(self, event):
        # This makes the inner container's width match the canvas width when the canvas resizes
        self.canvas.itemconfig(self.container_id, width=event.width)
        # Ensure scroll region is updated immediately after width change
        self.canvas.configure(scrollregion=self.canvas.bbox("all"))

    def toggle_area_options(self, area_num, initial=False):
        # This function is now only relevant for Area 1
        if area_num == 1:
            current_visibility = self.area1_options_visible.get()
            if not initial:
                self.area1_options_visible.set(not current_visibility)
            if self.area1_options_visible.get():
                self.area1_inner_frame.pack(fill="x", padx=10, pady=5)
                self.area1_toggle_btn.config(text="▼ Hide Measure Mode Options")
            else:
                self.area1_inner_frame.pack_forget()
                self.area1_toggle_btn.config(text="► Show Measure Mode Options")

        self.on_frame_configure(None) # Call to update scroll region

    def toggle_load_setup_options(self, initial=False):
        current_visibility = self.load_setup_options_visible.get()
        if not initial:
            self.load_setup_options_visible.set(not current_visibility)
        if self.load_setup_options_visible.get():
            self.load_setup_inner_frame.pack(fill="x", padx=10, pady=5)
            self.load_setup_toggle_btn.config(text="▼ Hide Load Setup Options")
        else:
            self.load_setup_inner_frame.pack_forget()
            self.load_setup_toggle_btn.config(text="► Show Load Setup Options")
        self.on_frame_configure(None) # Call to update scroll region

    def toggle_area_entries(self):
        # Area 1 time range entries are now always enabled for editing
        self.area1_start_entry.config(state="normal")
        self.area1_end_entry.config(state="normal")
        

    def log(self, msg, tag=None):
        timestamp = datetime.now().strftime("%Y-%m-%d %H:%M:%S")
        line = f"[{timestamp}] {msg}\n"
        self.output.config(state="normal")
        self.output.insert(tk.END, line)
        if tag:
            self.output.tag_add(tag, f"end-{len(line)}c", "end-1c")
        self.output.config(state="disabled")
        self.output.see(tk.END)
        self.log_data.append([timestamp, msg])

    def clear_log(self):
        self.output.config(state="normal")
        self.output.delete("1.0", tk.END)
        self.output.config(state="disabled")
        self.log_data.clear()

    def connect(self):
        ip = self.ip_address.get()
        try:
            self.sock = socket.create_connection((ip, self.port), timeout=3)
            self.status_label.config(text=f"🟢 Connected to {ip}:{self.port}", foreground="green")
            self.log(f"✅ Connected to DL950 at {ip}:{self.port}", tag="info")

        except Exception as e:
            messagebox.showerror("Connection Failed", str(e))
            self.sock = None
            self.status_label.config(text="🔴 Not connected", foreground="red")

    def load_setup_from_entry(self):
        if not self.sock:
            self.log("❌ Not connected.", tag="error")
            return
        setup_name = self.setup_filename.get().strip()
        if setup_name:
            cmd = f':FILE:LOAD:SETUP "{setup_name}"'
            self.send_command_custom(cmd)
            self.log(f"📂 Requested setup load: {setup_name}", tag="info")
        else:
            self.log("⚠️ Please enter a setup filename.", tag="info")
            messagebox.showinfo("Missing Filename", "Please enter a setup filename to load.")

    def send_command(self):
        if not self.sock:
            self.log("❌ Not connected.", tag="error")
            return
        cmd = self.cmd_entry.get().strip()
        if cmd:
            self.send_command_custom(cmd, custom_cmd_origin=True)

    def send_command_custom(self, cmd, custom_cmd_origin=False, wait_for_response=False):
        try:
            self.sock.sendall((cmd + "\n").encode())
            if cmd.endswith("?"):
                # Give instrument a moment to process and respond
                self.sock.settimeout(1.0) # Set a short timeout for the read
                response = self.sock.recv(4096).decode().strip()
                self.sock.settimeout(None) # Reset to default (or whatever is appropriate)
                
                if not custom_cmd_origin:
                    self.log(f"> {cmd}", tag="cmd")
                else:
                    self.log(f"> {cmd}", tag="cmd")

                # Handle specific query responses
                if cmd.startswith(":MEASure:TRANGE1?"):
                    self.log(f"< Area 1 Time Range currently set to {response} (Divisions)", tag="resp")

                elif cmd == ":MEASure:MODE?":
                    mode_status = "ON" if response.strip().upper() in ["ON", "1"] else "OFF"
                    self.log(f"< Global Measurement Mode: {mode_status}", tag="resp")
                    self.area1_mode_on.set(response.strip().upper() in ["ON", "1"])

                # Handle generic measurement queries
                elif response.upper().startswith(":MEAS"):
                    self.parse_and_log_response(cmd, response)
                else:
                    self.log(f"< {response}", tag="resp")
                
                if wait_for_response:
                    return response
            else:
                self.log(f"> {cmd} sent (no response expected)", tag="cmd")
            return None
        except socket.timeout:
             self.log(f"❌ Communication error: Timeout waiting for response to '{cmd}'", tag="error")
             return None
        except Exception as e:
            self.log(f"❌ Communication error: {e}", tag="error")
            return None

    def parse_and_log_response(self, original_cmd, response):
        try:
            area_label = "Area 1"
            # Retain Area 2 checks in parser logic, although not triggered by UI
            if ":MEASURE:AREA2:" in original_cmd.upper() or ":MEASure:TRANGE2?" in original_cmd.upper() or ":MEASURE:AREA2:MODE?" in original_cmd.upper():
                area_label = "Area 2"

            cmd_tokens = original_cmd.upper().split(':')
            ch = "N/A"
            sch = "N/A"
            param_key = "N/A"

            # Parse CH, SCH, and Parameter from the original command
            for i, token in enumerate(cmd_tokens):
                if "CHAN" in token:
                    ch_part = re.search(r'\d+', token.replace("CHAN", "").replace("NEL", ""))
                    if ch_part:
                        ch = ch_part.group(0)
                elif "SCH" in token:
                    sch_part = re.search(r'\d+', token.replace("SCH", "").replace("ANNEL", ""))
                    if sch_part:
                        sch = sch_part.group(0)
                elif any(p.upper().startswith(token) for p in self.all_parameters):
                    param_key = token
                    break
            
            # Map short key to full name for better log clarity
            param_name = {
                "AMPL": "Amplitude", "AVER": "Average", "AVGF": "Avg Frequency", "AVGP": "Avg Period",
                "BWID": "Bandwidth", "DUTY": "Duty Cycle", "FALL": "Fall Time", "FREQ": "Frequency",
                "HIGH": "High", "LOW": "Low", "MAX": "Maximum", "MIDD": "Middle", "MIN": "Minimum",
                "NOV": "Neg. Overshoot", "NWID": "Neg. Width", "PER": "Period", "PNUM": "Pulse Count",
                "POV": "Pos. Overshoot", "PTOP": "Peak-to-Peak", "PWID": "Pulse Width", "RISE": "Rise Time",
                "RMS": "RMS", "SDEV": "Std Dev", "TY1": "TY1 Integral", "TY2": "TY2 Integral"
            }.get(param_key[:4], param_key)

            # Check for State/Mode responses
            if original_cmd.upper().endswith(("STATE?", "MODE?")):
                state_value = response.strip().upper()
                state_text = "ON" if state_value in ["1", "ON"] else ("OFF" if state_value in ["0", "OFF"] else f"UNKNOWN({response})")

                if ":MEASURE:MODE?" in original_cmd.upper():
                    self.log(f"Global Measurement Mode: {state_text}", tag="resp")
                elif ":MEASURE:AREA2:MODE?" in original_cmd.upper():
                    self.log(f"Area 2 Measurement State: {state_text}", tag="resp")
                else:
                    self.log(f"{area_label} - {param_name} on CH{ch} SCH{sch} State: {state_text}", tag="resp")
            else:
                # Handle value responses
                match = re.match(r"([-+]?\d*\.?\d+(?:[Ee][-+]?\d+)?)([A-Za-z%µΩ]*)", response.strip())

                if match:
                    value_part = match.group(1)
                    unit_part = match.group(2)

                    if value_part.upper() == "NAN":
                        display_value = "NAN"
                    else:
                        display_value = f"{value_part}{unit_part}"
                else:
                    display_value = response.strip()

                self.log(f"{area_label} - {param_name} on CH{ch} SCH{sch} = {display_value}", tag="resp")

        except Exception as e:
            self.log(f"< {response} (general parse error in context of command '{original_cmd}': {e})", tag="error")


    def get_unit(self, param):
        param = param.upper()
        if "FREQ" in param:
            return "Hz"
        elif "PER" in param or "RISE" in param or "FALL" in param or "PWID" in param or "BWID" in param:
            return "s"
        elif "RMS" in param or "AMPL" in param or "MAX" in param or "MIN" in param or "HIGH" in param or "LOW" in param or "PTOP" in param or "MIDD" in param:
            return "V"
        elif "DUTY" in param or "OVERSHOOT" in param:
            return "%"
        elif "SDEV" in param:
            return "V"
        elif "NUM" in param:
            return "count"
        elif "INTEG" in param:
            return "V*s"
        else:
            return ""

    def validate_time_range_input(self, start_str, end_str, area_name):
        try:
            start_val = float(start_str)
            end_val = float(end_str)
            if not (-5.00 <= start_val <= 5.00 and -5.00 <= end_val <= 5.00):
                messagebox.showerror("Invalid Input", f"Time Range for {area_name} values must be between -5.00 and 5.00 divisions.")
                self.log(f"❌ Time Range for {area_name} values out of bounds (-5.00 to 5.00).", tag="error")
                return None, None
            return start_val, end_val
        except ValueError:
            messagebox.showerror("Invalid Input", f"Please enter valid numbers for Time Range for {area_name}.")
            self.log(f"❌ Invalid input for Time Range for {area_name}. Please enter numbers.", tag="error")
            return None, None

    def set_area1_time_range(self):
        if not self.sock:
            self.log("❌ Not connected.", tag="error")
            return

        start_val, end_val = self.validate_time_range_input(self.area1_start.get(), self.area1_end.get(), "Area 1")
        if start_val is None:
            return

        cmd = f":MEASure:TRANGE1 {start_val},{end_val}"
        self.send_command_custom(cmd)
        self.log(f"⏱️ Successfully sent command to set Area 1 Time Range.", tag="info")

    def set_area1_mode_on(self):
        if not self.sock:
            self.log("❌ Not connected.", tag="error")
            return
        cmd = ":MEASure:MODE ON"
        self.send_command_custom(cmd)
        self.log("✅ Sent command to set Global Measurement Mode ON.", tag="info")
        self.area1_mode_on.set(True) # Update UI state for Area 1 mode
        time.sleep(0.5) # Increased delay for the device to process

    def set_area1_mode_off(self):
        if not self.sock:
            self.log("❌ Not connected.", tag="error")
            return
        # 1. Send the command to turn global measurement mode OFF
        cmd = ":MEASure:MODE OFF"
        self.send_command_custom(cmd)
        
        # 2. Update the internal state variables
        self.log("🛑 Sent command to set Global Measurement Mode OFF.", tag="info")
        self.area1_mode_on.set(False) # Update UI state for Area 1 mode
        # Removed area 2 mode off check/logic

    def query_area1_mode_status(self):
        if not self.sock:
            self.log("❌ Not connected.", tag="error")
            return
        self.log("❓ Querying Global Measurement Mode status...", tag="info")
        self.send_command_custom(":MEASure:MODE?", wait_for_response=True)

    def query_area1_measurements(self):
        if not self.sock:
            self.log("❌ Not connected.", tag="error")
            return
        if not self.area1_mode_on.get():
            self.log("⚠️ Global Measure Mode must be ON to query Area 1 measurements.", tag="error")
            messagebox.showerror("Operation Blocked", "Please turn on Global Measure Mode first.")
            return

        self.log("📊 Starting Area 1 measurement query...", tag="info")

        selected_ch_schs = [(ch, sch) for (ch, sch), var in self.channel_subchannel_checks.items() if var.get()]
        selected_area1_params = [p for p, var in self.area1_param_checks.items() if var.get()]

        if not selected_ch_schs:
            self.log("⚠️ Select at least one channel+subchannel to get Area 1 measurements.", tag="info")
            return
        if not selected_area1_params:
            self.log("⚠️ No parameters selected for Area 1. Skipping Area 1 queries.", tag="info")
            return

        self.send_command_custom(":MEASure:TRANGE1?")

        self.log("\n--- Querying parameters for Area 1 ---", tag="info")
        for ch, sch in selected_ch_schs:
            for param in selected_area1_params:
                # For Area 1, assuming standard measurement commands without an explicit AREA1 prefix
                cmd = f":MEASure:CHANnel{ch}:SCHannel{sch}:{param}:VALue?"
                self.send_command_custom(cmd)

        self.log("\n📊 Area 1 measurement query complete.", tag="info")

    
    def start_acquisition(self):
        if not self.sock:
            self.log("❌ Not connected.", tag="error")
            return
        try:
            self.sock.sendall(b":START\n")
            self.log("> :START", tag="cmd")
            self.log("✅ Acquisition started.", tag="info")
        except Exception as e:
            self.log(f"❌ Error starting acquisition: {e}", tag="error")

    def stop_acquisition(self):
        if not self.sock:
            self.log("❌ Not connected.", tag="error")
            return
        try:
            self.sock.sendall(b":STOP\n")
            self.log("> :STOP\n", tag="cmd")
            self.log("🛑 Acquisition stopped.", tag="info")
        except Exception as e:
            self.log(f"❌ Error stopping acquisition: {e}", tag="error")

    def save_log_to_csv(self):
        if not self.log_data:
            messagebox.showinfo("No Data", "No log data to save.")
            return
        filepath = filedialog.asksaveasfilename(
            defaultextension=".csv",
            filetypes=[("CSV files", "*.csv"), ("All files", "*.*")],
            title="Save Log As"
        )
        if filepath:
            try:
                with open(filepath, 'w', newline='', encoding='utf-8') as csvfile:
                    csv_writer = csv.writer(csvfile)
                    csv_writer.writerow(["Timestamp", "Log Message"]) # Write header
                    csv_writer.writerows(self.log_data)
                messagebox.showinfo("Save Successful", f"Log saved to {filepath}")
                self.log(f"💾 Log data saved to {filepath}", tag="info")
            except Exception as e:
                messagebox.showerror("Save Error", f"Failed to save log: {e}")
                self.log(f"❌ Error saving log to CSV: {e}", tag="error")

    def _load_image_into_label(self, filepath):
        """Helper method to load an image using PIL and update the logo_label."""
        try:
            # Open image using PIL
            pil_image = Image.open(filepath)
            # Resize image if necessary (e.g., to 1/3rd of original size)
            width, height = pil_image.size
            pil_image = pil_image.resize((width // 3, height // 3), Image.LANCZOS)
            
            # Convert PIL image to PhotoImage
            self.logo = ImageTk.PhotoImage(pil_image)
            self.logo_label.config(image=self.logo, text="") # Update image and clear text
        except FileNotFoundError:
            self.logo_label.config(text="Yokogawa Logo", font=("Segoe UI", 9, "italic"), image="")
            self.log(f"❌ Logo image not found at: {filepath}", tag="error")
        except Exception as e:
            self.logo_label.config(text="Yokogawa Logo", font=("Segoe UI", 9, "italic"), image="")
            self.log(f"❌ Error loading image: {e}", tag="error")

if __name__ == '__main__':
    root = tk.Tk()
    app = DL950SocketSCPIApp(root)
    root.mainloop()
